/* Copyright 2009 Sony Mathew, Minneapolis MN. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at  http://www.apache.org/licenses/LICENSE-2.0  Unless required by applicable law or agreed to in writing, software  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */
package com.xsm.gwt.widgets.client;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;

/**
 * An enhanced FlexTable.
 * 
 * Within a single fluent expression you target operations at one of the following:
 *  table  : at start of expression when you create an instance
 *  row    : when u nextRow() or row(i)
 *  column : when u column(i)
 *  cell   : when u cell(i,j)
 * 
 * cell(i,j) explicitly targets a cell for operations in the expression,
 *  so table/row/column specific operations will cause an exception.
 *  put(widget) will place widget in this explicitly targetted cell.
 *  
 * nextRow()/row(i)/column(i) explicitly targets a row/column for further operatoins in the expression.
 *  put(widget) will place widget in next cell of targetted row or column.
 *   if row is being targeted then put(w) will increment column.
 *   if column is being targeted then put(w) will increment row.
 *  Essentially, it works as normally expected as you build your expression. 
 * 
 * Note: put(widget) cell becomes the implicitly targetted cell
 *   so after a put(w) all operations now target this cell.
 * 
 * Sample usage:
 *  
 *  AxisTable t = new AxisTable()
 *      .row(1).css("row-normal")
 *          .put("Hello")  //add to implicit current|target cell then increment to next column.
 *          .put(button1)  //add to implicit current|target cell then increment to next column.
 *          .put(label1)
 *      .nextRow().css("row-even")
 *          .put(button1)
 *          .put(button2)
 *          .put(label1)       
 *      .cell(0,1).css("special-cell").span(2,3) //row & column span.
 *          .put("Hello") //add to cell (0, 1) explicity targetted just prior.
 *  
 * Caution: HTML Table design and rendering:
 *  - <TR> and <TD> represent rows and cells of a logical table.
 *  - The logical table rows/cols are mapped to a display table with more/less rows/cols.
 *  - The logical table and display table map 1 to 1 if NOT using rowspan/colspan (things are easy).
 *  - See HTML docs for how rowspan/colspan affects logical to display table row/col mappings.
 *      e.g.if a logical table cell spans 2 rows then the logical cell below it
 *          in the next row will shift right in the display table by column+1,
 *      e.g.if a logical table cell spans 2 columns then the next logical cell to its right
 *          in the same row will shift right in the display table by column+1.
 *      e.g.if a row has fewer cells than the largest row, then empty cells are automatically added.
 *                      
 *  - GWT's FlexTable indexes represent logical row and cell indexes always (i.e. <TR> and <TD>).
 *      Note: Skipped intermediate rows or cells automatically generate cells.
 *  - GWT's FlexTable rowspan and colspan do not affect logical indexes - only affects their mappings to display table.
 *    
 * XXX: Better design for styling based on target of table/row/column/cell/content
 *  
 * @author Sony Mathew
 */
public class AxisTable extends FlexTable {

    /**
     * Indicates the different items we can target for actions 
     *  within the AxisTable (does not apply to the X or Y Panel targets 
     *      that are contents within a Table cell).
     */
    enum TableTargetType { 
        TABLE, TABLE_ROW, TABLE_COLUMN, TABLE_CELL 
    }
    
    //-1 indicates no table cell is implicitly targetted.
    private int targetRow = -1;
    private int targetColumn = -1
    ;
    private TableTargetType tableTargetType = TableTargetType.TABLE;
    
    /**
     * Change target to given row = i in this AxisTable.
     * All put(w) will now add cells as columns to this row.
     * 
     * author Sony Mathew
     */
    public AxisTable row(int i) {
        tableTargetType = TableTargetType.TABLE_ROW;
        targetRow = i;
        targetColumn = -1;
        return this;        
    }

    /**
     * Appends a new row to the end and now targets this row.
     *  Same as row(table.getRowCount()).
     * 
     * @see #row(int)
     * author Sony Mathew
     */
    public AxisTable nextRow() {
        return row(getRowCount());
    }

    /**
     * Targets the last existing row.
     *  Same as row(table.getRowCount()-1).
     * 
     * @see #row(int)
     * author Sony Mathew
     */
    public AxisTable lastRow() {
        return row(getRowCount()-1);
    }
    
    /**
     * Change target to given column = i in this AxisTable.
     * 
     * All put(w) will now add cells as rows to this column.
     * 
     * author Sony Mathew
     */
    public AxisTable column(int i) {
        tableTargetType = TableTargetType.TABLE_COLUMN;
        targetRow = -1;
        targetColumn = i;
        return this;        
    }
    
    /**
     * Change target to specific cell at row, col in this AxisTable.
     * 
     * Next put(w) will add to this specific cell.
     * 
     * author Sony Mathew
     */
    public AxisTable cell(int row, int col) {
        tableTargetType = TableTargetType.TABLE_CELL;
        targetRow = row;
        targetColumn = col;        
        return this;
    }
       
    /**
     * Place one widget at the currently targetted cell.
     *  The cell can be explicitly targetted using cell(i,j) or implicitly targetted
     *      by targetting row or column (the implicit target cell is the next available
     *          cell in the target row or column).
     *      
     * Important: 
     *  The cell is computed first, then widget is placed.
     *    Increments the cell in the target row or column.
     *    so if current target is row, then increments column number.
     *    so if current target is column, then increments row number.
     * 
     * All operations will now target this implicit or explicit cell.
     * 
     * If a null widget provided, it is safely ignored, with no effect i.e. no-op.
     * 
     * author Sony Mathew
     */
    public AxisTable put(Widget w) {
        if (w == null) {
            return this;
        }
        
        if (tableTargetType == TableTargetType.TABLE_ROW) {
            targetColumn++;
        }else if (tableTargetType == TableTargetType.TABLE_COLUMN) {
            targetRow++;
        }else if (tableTargetType == TableTargetType.TABLE_CELL) {
            //nothing to do, explicitly targetting cell already.
        }else{
            throw new IllegalStateException("Illegal call to put(w), not targetting row or column");
        }
        
        addTargetContent(w);
        return this;
    }

    /**
     * Shortcut for adding a Label.
     * @see #put(Widget)
     * 
     * author Sony Mathew
     */
    public AxisTable put(String labelText) {
        put(new Label(labelText));
        return this;
    }
    

    /** 
     * If the target type is table, row or column
     *  a cell may be implicitly targetted with each put(widget). 
     */
    private boolean isTableCellImplicitTarget() {
        return (targetRow >= 0 && targetColumn >=0);
    }

    /**
     * Throws exception if not targetting a valid row or column.
     * author Sony Mathew
     */
    private void checkTargettingTableCell() {
        if (targetRow < 0 || targetColumn < 0) {
            throw new IllegalStateException("Illegal target row[" + targetRow + 
                    "], column[" + targetColumn + "]");
        }
    }
    
    /**
     * Add widgets to the current target panel being built.
     */
    private void addTargetContent(Widget...ws) {
        if (ws == null || ws.length == 0) {
            return;
        }        
        if (tableTargetType == TableTargetType.TABLE) {
            throw new IllegalStateException("Can only add widgets to a table cell");
        }
        if (ws.length != 1) {
            throw new IllegalStateException("Cannot add more than 1 widget to a cell"); 
        }
        Widget w = ws[0];
        setTargetCellContent(w);            
    }
    
    /**
     * Set the widget of the currently targetted cell.
     * 
     * author Sony Mathew
     */
    private void setTargetCellContent(Widget w) {
        checkTargettingTableCell();
        setWidget(targetRow, targetColumn, w);
    }
    
       
    /**
     * Set the id of the target.
     * 
     * Cannot set id of row or column if they are the current targets.
     * 
     * TODO: Fix this.
     * Unlike AxisPanel ids, cell content are not prefixed with 
     * table's id.
     * 
     * @see AxisPanelStyle#id(String)
     */
    public AxisTable id(String id) {        
        if (tableTargetType != TableTargetType.TABLE) {
            throw new IllegalStateException("Cannot set id for individual row/colum/cell");
        }
        DOM.setElementAttribute(getElement(), "id", id);
        return this;
    }
    
    /**
     * Set the style of target: table, row or cell.
     *  Note: cannot style column.
     *  Note: cannot style rows until atleast one widget has been placed in that row
     *      Limiations of GWT. Use row(i) or lastRow() to reference a row to style after.
     * 
     * @see AxisPanelStyle#styke(String,String)
     */
    public AxisTable style(String styleAttrib, String styleValue) {
        Element e = null;
        
        if (isTableCellImplicitTarget() || tableTargetType == TableTargetType.TABLE_CELL) {
            e = getFlexCellFormatter().getElement(targetRow, targetColumn);        
        }else if (tableTargetType == TableTargetType.TABLE) {
            e = getElement();
        }else if (tableTargetType == TableTargetType.TABLE_ROW) {
            getRowFormatter().getElement(targetRow);
        }else if (tableTargetType == TableTargetType.TABLE_COLUMN) {
            throw new IllegalStateException("Cannot style a table column");
        }else{
            throw new IllegalStateException("Unexpected table target");
        }
        
        DOM.setStyleAttribute(e, styleAttrib, styleValue);
        return this;
    }
    
    /**
     * Convenience for creating a solid border using styles with the given width and color.
     */
    public AxisTable styleBorder(int width, String color) {
        style("borderStyle", "solid");
        style("borderColor", color);
        style("borderWidth", ""+width);
        return this;
    }

    /**
     * Convenience for creating a solid border using styles with default color and the given width. 
     */
    public AxisTable styleBorder(int width) {
        style("borderStyle", "solid");
        style("borderColor", "#BADBEB");
        style("borderWidth", "1");
        return this;
    }
    
    /**
     * Set the css class name of target: table, row, column, cell.
     * 
     * @see AxisPanelStyle#css(String)
     */
    public AxisTable css(String cssName) {
        if (isTableCellImplicitTarget() || tableTargetType == TableTargetType.TABLE_CELL) {
            getFlexCellFormatter().addStyleName(targetRow, targetColumn, cssName);
        }else if (tableTargetType == TableTargetType.TABLE) {
            addStyleName(cssName);
        }else if (tableTargetType == TableTargetType.TABLE_ROW) {
            getRowFormatter().addStyleName(targetRow, cssName);
        }else if (tableTargetType == TableTargetType.TABLE_COLUMN) {
            getColumnFormatter().addStyleName(targetColumn, cssName);
        }else{
            throw new IllegalStateException("Unexpected table target");
        }
        return this;
    }

    /**
     * Set the border of target: table, cell-content.
     * 
     * @see AxisPanelStyle#border(int)
     */
    public AxisTable border(int borderWidth) {
        if (tableTargetType == TableTargetType.TABLE) {
            setBorderWidth(borderWidth);
        }else{
            throw new IllegalStateException("Cannot set border width for individual rows/colums/cells");
        }
        return this;
    }
    
    /**
     * Set the pixel size of target: table, cell-content.
     * 
     * @see AxisPanelStyle#size(int, int)
     */
    public AxisTable size(int pixelWidth, int pixelHeight) {
        if (tableTargetType == TableTargetType.TABLE) {
            setPixelSize(pixelWidth, pixelHeight);
        }else{
//              XXX: Change this.
            throw new IllegalStateException("Cannot set sizes for individual rows/colums/cells");
        }
        return this;
    }

    /**
     * Set the css size of table.
     * 
     * @see AxisPanelStyle#size(String, String)
     */
    public AxisTable size(String cssWidth, String cssHeight) {
        width(cssWidth);
        height(cssHeight);
        return this;
    }
    
    /**
     * Set the css width of table.
     * 
     * @see AxisPanelStyle#width(String)
     */
    public AxisTable width(String cssWidth) {
        if (tableTargetType == TableTargetType.TABLE) {
            setWidth(cssWidth);
        }else{
//              XXX: Change this
            throw new IllegalStateException("Cannot set sizes for individual rows/colums/cells");
        }
        return this;
    }
    
    /**
     * Set the css height of table.
     * 
     * @see AxisPanelStyle#height(String)
     */
    public AxisTable height(String cssHeight) {
        if (tableTargetType == TableTargetType.TABLE) {
            setHeight(cssHeight);
        }else{
//              XXX: Change this
            throw new IllegalStateException("Cannot set sizes for individual rows/colums/cells");
        }
        return this;
    }
    
    /**
     * Set spacing between (around) cells of table.  
     * 
     * @see AxisPanelStyle#space(int)
     */
    public AxisTable space(int pixels) {
        if (tableTargetType == TableTargetType.TABLE) {
            setCellSpacing(pixels);
        }else{
//              TODO: Change this
            throw new IllegalStateException("Cannot set spacing for individual rows/colums/cells");
        }
        return this;
    }

    /**
     * Set padding of all cells of table.  
     * 
     * @see AxisPanelStyle#space(int)
     */
    public AxisTable pad(int pixels) {
        if (tableTargetType == TableTargetType.TABLE) {
            setCellPadding(pixels);
        }else{
//              XXX: Change this
            throw new IllegalStateException("Cannot set padding for individual rows/colums/cells");
        }
        return this;
    }
    
    /**
     * Set alignment for target row or column.
     *  When target row or column changes, its starts with the default alignment.
     *    of 0,0.
     * You must set this before adding content to the cell using put(w).
     * 
     * XXX: Throw IllegalStateException if attempting to align before put(w).
     *  Note: getWidget(row,col) will throw IndexOutOfBoundException, 
     *      so find another way.
     * 
     * @param x: -1, 0, 1 for horizontal left, center, right
     * @param y:  1, 0, -1 for vertical top, middle, bottom.
     * 
     * @see AxisPanelStyle#align(int, int)
     */
    public AxisTable align(int x, int y) {
        checkTargettingTableCell();
        getFlexCellFormatter().setAlignment(targetRow, targetColumn, 
                AxisPanelStyle.fromAlignX(x), 
                    AxisPanelStyle.fromAlignY(y));
        return this;
    }
    
    /**
     * Changes target rowSpan and colSpan for the target cell.
     * Use nextRow(), row(i) or cell(row,col) to target a cell.
     * 
     * author Sony Mathew
     */
    public AxisTable span(int rowSpan, int colSpan) {
        checkTargettingTableCell();        
        FlexCellFormatter cf = getFlexCellFormatter();
        cf.setRowSpan(targetRow, targetColumn, rowSpan);
        cf.setColSpan(targetRow, targetColumn, colSpan);
        return this;
    }
    
    /**
     * Creates an overlay. (See Overlay).
     * 
     * author Sony Mathew
     */
    public Overlay createOverlay(int startRow, int startCol, int endRow, int endCol) {
        return new Overlay(startRow, startCol, endRow, endCol);
    }

    /**
     * Creates an overlay. (See Overlay).
     * 
     * author Sony Mathew
     */
    public Overlay createOverlay(Widget...ws) {
        return new Overlay(ws);
    }
    
    /**
     * Overlays a widget given the indexes to overlay on.
     */
    public class Overlay {
        private int startRow, startCol, endRow, endCol, rowSpan, colSpan = -1;
        private Widget w = null;
        
        /**
         * Provide the bounds within the logical table to overlay.
         *  bounds are inclusive of the indexes provided. 
         *      indexes are 0 based into the AxisTable.
         *      
         * Important Note:
         *   Widgets are indexed into a logical table. rowSpan and colSpan are 
         *   computed only in the final displayed table and should NOT be considered 
         *   when specifying the bounds to overlay within the logical table.
         *       
         * author Sony Mathew
         */
        public Overlay(int startRow, int startCol, int endRow, int endCol) {
            this.startRow = startRow;
            this.startCol = startCol;
            this.endRow = endRow;
            this.endCol = endCol;
            this.rowSpan = endRow-startRow+1;
            this.colSpan = endCol-startCol+1;
        }
        
        /**
         * Provide the bounds within the logical table to overlay by giving the widgets that should be covered-up.
         * 
         * @see extendsBounds(Widget)
         * 
         * author Sony Mathew
         */
        public Overlay(Widget...ws) {
            this.startRow = Integer.MAX_VALUE;
            this.startCol = Integer.MAX_VALUE;
            this.endRow = -1;
            this.endCol = -1;
            this.rowSpan = -1;
            this.colSpan = -1;            
            extendBounds(ws);            
        }
        
        /**
         * Extend the rectangle bounds of this overaly to include covering up the given Widget(s).
         * 
         * Note: this changes the span automatically to occupy the new space. 
         * 
         * author Sony Mathew
         */
        public Overlay extendBounds(Widget...ws) throws OverlayWidgetsNotFoundExeption {
            int widgetsFound = 0;
            
            for(int row=0;row<getRowCount();row++) {
                for(int cell=0;cell<getCellCount(row);cell++) {
                    Widget cellw = getWidget(row, cell);
                    for(Widget w : ws) {
                        if (w == cellw) {
                            startRow = row<startRow? row : startRow;
                            startCol = cell<startCol? cell : startCol;
                            endRow = row>endRow? row : endRow;
                            endCol = cell>endCol? cell : endCol;
                            rowSpan = endRow-startRow+1;
                            colSpan = endCol-startCol+1; 
                            widgetsFound++;
                        }
                    }
                }
            } 
            
            if (widgetsFound != ws.length) {
                throw new OverlayWidgetsNotFoundExeption(ws);
            }
            return this;
        }
        
        /**
         * Clear the table to make way for the overlay.
         * 
         * author Sony Mathew
         */
        private void clearForOverlay() {
            for(int row=endRow; row>=startRow; row--) {
                for(int col=endCol; col>=startCol; col--) {
                    if (isCellPresent(row,col)) {
                        getWidget(row,col);
                        removeCell(row,col);
                    }
                }
            }            
        }

        /**
         * Provide the widget to overlay (but does not overlay, use overlay() to explicitly do so).
         * 
         * author Sony Mathew
         */
        public void setWidget(Widget w) {
            this.w = w;
        }
        
        /**
         * Same as setWidget(w).
         * 
         * author Sony Mathew
         */
        public Overlay put(Widget w) {
            this.setWidget(w);
            return this;
        }
        
        /**
         * Change the default span (which = overlay bounds) to a different span.
         * Specify a new span if the original content that was taking up the space in the bounds 
         *  had a span outside those bounds.
         *  
         * author Sony Mathew
         */
        public Overlay span(int rowSpan, int colSpan) {
            this.rowSpan = rowSpan;
            this.colSpan = colSpan;
            return this;
        }
        
        /**
         * Indicate if we have already overlayed.
         * author Sony Mathew
         */
        public boolean isOverlayed() {
            if (isCellPresent(startRow, startCol)) {
                Widget cellw = getWidget(startRow, startCol);
                if (cellw == this.w) {
                    return true;
                }
            }
            return false;
        }
        
        /**
         * Overlay the widget that was provided before into the bounds specified before.
         * All widgets in the bounds specified within the table are removed and replaced by the overlay widget.
         * 
         * Important Note: 
         *   Existing widgets can shift (i.e. indexes can be different after overlay()).
         *      So another Overlay instance's bounds can be affected accordingly.
         *   The widget replacement will by default span the bounds.  
         *   Change default span by re-specifying a different span(rowSpan, colSpan) on the Overlay. 
         *      e.g. if original widgets spanned outside the bounds, you should respecify span on the Overlay 
         *          so as to take up the same space in the final displayed table.
         *         
         *   
         * author Sony Mathew
         */
        public Overlay overlay() {
            if (this.w == null) {
                throw new IllegalStateException("No overlay widget provided");
            }
            if (isOverlayed()) {
                return this; //already done.
            }
            clearForOverlay();
            AxisTable.this.insertCell(startRow, startCol);
            AxisTable.this.setWidget(startRow, startCol, w);
            FlexCellFormatter cf = getFlexCellFormatter();
            cf.setRowSpan(startRow, startCol, rowSpan);
            cf.setColSpan(startRow, startCol, colSpan);  
            return this;
        }
    }
    
    /**
     * Indicates that the widgets to overlay are not found in the table.
     */
    public static class OverlayWidgetsNotFoundExeption extends RuntimeException {

        private static final long serialVersionUID = 1L;
        private final Widget[] ws;
        
        public OverlayWidgetsNotFoundExeption(Widget[] ws) {
            super("Overlay widgets were not found");            
            this.ws = ws;
        }
        
        public Widget[] getWidgetsToOverlay() {
            return ws;
        }
    }
}
